AWS Solutions Constructを使用してOACとCloudFront ディストリビューションを簡単に実装できないか試してみた
はじめに
S3 オリジンへのアクセスをCloudFront ディストリビューションのみに制限する際に、OAI(Origin access identity)が使用されてきましたがセキュリティの観点などから現在はOAC(Origin access control)の使用が推奨されています。
Amazon CloudFront オリジンアクセスコントロール(OAC)のご紹介
CDKで構築する場合、L2コンストラクトがまだ提供されていないため、L1コンストラクトを使用する必要がありますが記述量が多くなってしまいます。
- L1コンストラクトを使用した実装例
そこで、もっと記述量を減らし簡単に実装できる方法を探してみたところ、AWS Solutions Constructsのaws-cloudfront-s3を使用する方法がありましたので、このブログで紹介したいと思います。npmの公式ページには、以下のようにOACを使うことが前提のモジュールであることが記載されています。
この方法を使うと、かなり記述量少なくOACを使用したオリジン設定が簡単に実装でき、OACについては特に記述しなくても最低限のバケットポリシーと共にデフォルトの設定で作成してくれます。
結論
すごく簡単に実装できました!!
AWS Solutions Constructsとは
AWS Solutions ConstructsはAWS CDKのオープンソース拡張機能で、公式ドキュメントには以下のように説明されています。
よく使われるアーキテクチャのパターンを再利用して構築できるようにモジュール化されたものですね。
使用したモジュールのバージョン
- aws-cdk: 2.131.0
- aws-cdk-lib: 2.150.0
- node js: 20.16.0
- typescript: 5.4.5
- @aws-solutions-constructs/aws-cloudfront-s3: 2.65.0
CDKのコード
CDKの全体のコードは以下の通りとなります。
import { CloudFrontToS3 } from "@aws-solutions-constructs/aws-cloudfront-s3";
import { aws_s3, aws_ssm, aws_cloudfront, aws_iam } from "aws-cdk-lib";
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
export class SampleStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps) {
super(scope, id, props);
const accountId = cdk.Stack.of(this).account;
const region = cdk.Stack.of(this).region;
const s3Bucket = new aws_s3.Bucket(this, `s3Bucket`, {
bucketName: `s3-bucket-${region}-${accountId}`,
removalPolicy: cdk.RemovalPolicy.DESTROY,
});
const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
existingBucketObj: s3Bucket,
cloudFrontDistributionProps: {
defaultBehavior: {
allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
viewerProtocolPolicy:
aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
},
});
// s3:ListBucketをバケットポリシーに追加
const bucketPolicy = new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
principals: [new aws_iam.ServicePrincipal("cloudfront.amazonaws.com")],
actions: ["s3:ListBucket"],
resources: [s3Bucket.bucketArn],
conditions: {
StringEquals: {
"AWS:SourceArn": `arn:aws:cloudfront::${
cdk.Stack.of(this).account
}:distribution/${
cloudFrontToS3.cloudFrontWebDistribution.distributionId
}`,
},
},
});
s3Bucket.addToResourcePolicy(bucketPolicy);
}
}
コードの解説
肝心なCloudFront ディストリビューションとOACの作成は以下のコードだけで構築できます。かなり記述が少なく済みますね!
const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
existingBucketObj: s3Bucket,
cloudFrontDistributionProps: {
defaultBehavior: {
allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
viewerProtocolPolicy:
aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
},
});
CloudFront ディストリビューションとOAC
existingBucketObj
- オリジンにしたいS3バケットを指定します。
defaultBehavior
aws-cdk-lib
のaws_cloudfront
を使って設定する時のdefaultBehavior
と同じようにビヘイビアを設定する箇所です。
ハマりポイント
aws-cdk-lib
のaws_cloudfront_origin
を使ってdefaultBehavior
のパラメーターとしてオリジンを設定してデプロイすると以下のエラーが発生します。
- コード
const cloudFrontToS3 = new CloudFrontToS3(this, "cloudfront-s3", {
cloudFrontDistributionProps: {
defaultBehavior: {
origin: new aws_cloudfront_origins.S3Origin(s3Bucket), // <-- ここでオリジンを設定するとエラーが発生
allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
viewerProtocolPolicy:
aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
},
},
});
- エラー内容
Cannot use both Origin Access Control and Origin AccessIdentity on an origin
2:46:50 PM | UPDATE_FAILED | AWS::CloudFront::Distribution | cloudfront-s3...CloudFrontDistribution
Resource handler returned message: "Invalid request provided: Cannot use both Origin Access Control and Origin AccessIdentity on an origin (Service: CloudFront, Status Code: 400, Request ID: 4ba4d9c0-5ed3-44c8-a197-467b790911af)" (RequestToken: 3407237b-8bed-a84c-ba8e-dadc893b3ff9, HandlerErrorCode: InvalidRequest)
defaultBehavior
の中でオリジンを設定すると従来のOAI(Origin AccessIdentity)を使って構築しようとするようです。このため、規定でOACを使ってオリジン設定する動きと競合して上記のエラーが発生するようです。
バケットポリシーの設定
ブロックパブリックアクセスを有効(デフォルトでは有効)にしているオリジンのバケットでは、OACが割り当てられているCloudFront ディストリビューションからのみのアクセスをバケットポリシーで許可する必要があります。今回のモジュール(aws-cloudfront-s3)を使ってデプロイすると、以下の通り、s3:GetObjectについては自動的に追加してくれます。
バケットポリシー
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::s3-bucket-ap-northeast-1-123456789012/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": <ディストリビューションのARN>
}
}
}
]
}
しかしながら、これだけではバケットに対象のオブジェクトが存在しない場合に返却されるHTTPステータスコードが403 Forbiddenとなってしまいます。このため、バケットポリシーにs3:ListBucketを追加して、バケットに対象のオブジェクトが存在しない場合にも404 Not Foundを返すようにしてユーザビリティを向上させておくのが良いかと思います。
バケットポリシーにs3:ListBucketを追加
const bucketPolicy = new aws_iam.PolicyStatement({
effect: aws_iam.Effect.ALLOW,
principals: [new aws_iam.ServicePrincipal("cloudfront.amazonaws.com")],
actions: ["s3:ListBucket"],
resources: [s3Bucket.bucketArn],
conditions: {
StringEquals: {
"AWS:SourceArn": `arn:aws:cloudfront::${
cdk.Stack.of(this).account
}:distribution/${
cloudFrontToS3.cloudFrontWebDistribution.distributionId
}`,
},
},
});
s3Bucket.addToResourcePolicy(bucketPolicy);
デプロイ
デプロイすると以下のようにOACが作成されています。
CloudFrontにリクエストを送信
デプロイ後、CloudFrontにリクエストを送信してみます。
オブジェクトが存在する場合
オリジンに設定したS3に保存したJSONファイルの中身が返ってきました。
$ curl -i https://<ディストリビューションドメイン名>.cloudfront.net/sample.json
HTTP/2 200
content-type: application/json
content-length: 33
date: Wed, 21 Aug 2024 06:29:19 GMT
last-modified: Wed, 21 Aug 2024 06:28:13 GMT
x-amz-expiration: expiry-date="Sun, 25 Aug 2024 00:00:00 GMT", rule-id="MzEzMDVhOGQtZDgyMi00NWE1LWI2ZTctNzc3ZjBlNDMzYWU3"
etag: "bd73e16217411a2b3780ab4bd30c1685"
x-amz-server-side-encryption: AES256
accept-ranges: bytes
server: AmazonS3
via: 1.1 0932afdcbb622a4425fd671f0d67863a.cloudfront.net (CloudFront)
content-security-policy: default-src 'none'; img-src 'self'; script-src 'self'; style-src 'self'; object-src 'none'
strict-transport-security: max-age=63072000; includeSubdomains; preload
x-content-type-options: nosniff
x-frame-options: DENY
x-xss-protection: 1; mode=block
x-cache: Miss from cloudfront
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: lDVMqh9nWpi0HD0WQVCRjFRZJ7LUZV170Z1PFObdqCB1-CIN02z0uw==
{
"message": "Hello, World!"
}
オブジェクトが存在しない場合
バケットポリシーにs3:ListBucketを追加したので、ステータスコード404とNoSuchKeyエラーが返ってきました。
$ curl -i https://<ディストリビューションドメイン名>.cloudfront.net/not-exist.json
HTTP/2 404
content-type: application/xml
date: Wed, 21 Aug 2024 06:32:31 GMT
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 e5907f334714433599a0e1b9c57f44d6.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: zTESYU-Moq5bN6AawBi7kZ9rstx1B_xGx7vnlNv3UKSk6Ry6m4iFJg==
<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Key>not-exist.json</Key><RequestId>4F7Q5XZZNCFA36AA</RequestId><HostId>MZi5IiPW906nsNGFNAGN6/V/0e6ztPxS0YhKHuUax74m+rHxEL/ywzsZSEPhWKbQkJEhvJXGd3E=</HostId></Error>%
以上。